##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'net/dns'
require 'resolv'

class MetasploitModule < Msf::Auxiliary

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'DNS Lookup Result Comparison',
      'Description'    => %q{
        This module can be used to determine differences
      in the cache entries between two DNS servers. This is
      primarily useful for detecting cache poisoning attacks,
      but can also be used to detect geo-location loadbalancing.
      },
      'Author'         => [ 'hdm' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
        ],
      'DisclosureDate' => 'Jul 21 2008'
      ))

      register_options(
        [
          OptAddress.new('BASEDNS', [ true,  'The DNS cache server to use as a baseline', '4.2.2.3' ]),
          OptAddress.new('TARGDNS', [ true,  'The DNS cache server to test', nil ]),
          OptString.new('NAMES',    [ true,  'The list of host names that should be tested (comma separated)', 'www.google.com,www.yahoo.com,www.msn.com']),
          OptBool.new('CHECK_AUTHORITY',  [ false, 'Set this to true to verify authority records', false ]),
          OptBool.new('CHECK_ADDITIONAL', [ false, 'Set this to true to verify additional records', false ]),

        ])

  end


  def run
    base_addr = datastore['BASEDNS']
    targ_addr = datastore['TARGDNS']
    check_ar  = datastore['CHECK_ADDITIONAL']
    check_aa  = datastore['CHECK_AUTHORITY']
    names     = datastore['NAMES'].split(",").map {|c| c.strip }
    recurse   = true
    results   = {}

    print_status("Comparing results between #{base_addr} and #{targ_addr}...")

    base_sock = Rex::Socket.create_udp(
      'PeerHost' => base_addr,
      'PeerPort' => 53
    )

    targ_sock = Rex::Socket.create_udp(
      'PeerHost' => targ_addr,
      'PeerPort' => 53
    )

    names.each do |entry|
      entry.strip!
      next if (entry.length == 0)

      req = Resolv::DNS::Message.new
      req.add_question(entry, Resolv::DNS::Resource::IN::A)
      req.rd = recurse ? 1 : 0

      buf = req.encode
      print_status("Querying servers for #{entry}...")
      base_sock.put(buf)
      targ_sock.put(buf)

      base_res, base_saddr = base_sock.recvfrom(65535, 3.0)
      targ_res, targ_saddr = targ_sock.recvfrom(65535, 3.0)

      if !(base_res and targ_res and base_res.length > 0 and targ_res.length > 0)
        print_error("  Error: The baseline server did not respond to our request.") if ! (base_res and base_res.length > 0)
        print_error("  Error: The target server did not respond to our request.") if ! (targ_res and targ_res.length > 0)
        next
      end

      base_res = Resolv::DNS::Message.decode(base_res)
      targ_res = Resolv::DNS::Message.decode(targ_res)

      [base_res, targ_res].each do |res|
        hkey = (res == base_res) ? :base : :targ

        rrset = res.answer
        rrset += res.authority  if check_aa
        rrset += res.additional if check_ar

        (rrset).each do |ref|
          name,ttl,data = ref

          name = name.to_s
          anst = data.class.to_s.gsub(/^.*Resolv::DNS::Resource::IN::/, '')
          case data
          when Resolv::DNS::Resource::IN::NS
            data = data.name.to_s
          when Resolv::DNS::Resource::IN::MX
            data = data.exchange.to_s
          when Resolv::DNS::Resource::IN::A
            data = data.address.to_s
          when Resolv::DNS::Resource::IN::TXT
            data = data.strings.join
          when Resolv::DNS::Resource::IN::CNAME
            data = data.name.to_s
          else
            data = anst
          end

          results[entry]||={}
          results[entry][hkey]||={}
          results[entry][hkey][anst]||=[]
          results[entry][hkey][anst] << data
        end
      end
    end

    [ base_sock, targ_sock ].each {|s| s.close }


    print_status("Analyzing results for #{results.keys.length} entries...")

    results.each_key do |entry|

      n_add = []
      n_sub = []

      # Look for additional entries in the target NS
      if(results[entry][:targ])
        results[entry][:targ].each_key do |rtype|
          if(not results[entry][:base] or not results[entry][:base][rtype])
            results[entry][:targ][rtype].sort.each do |ref|
              n_sub << ("  + #{entry} #{rtype} #{ref}")
            end
          end
        end
      end

      if (results[entry][:base])
        results[entry][:base].each_key do |rtype|

          # Look for missing entries in the target NS
          if(not results[entry][:targ] or not results[entry][:targ][rtype])
            results[entry][:base][rtype].sort.each do |ref|
              n_sub << ("  - #{entry} #{rtype} #{ref}")
            end
            next
          end

          # Look for differences
          if( results[entry][:base][rtype].sort !=  results[entry][:targ][rtype].sort )
            results[entry][:base][rtype].sort.each do |ref|
              if(not results[entry][:targ][rtype].include?(ref))
                n_sub << ("  - #{entry} #{rtype} #{ref}")
              end
            end
            results[entry][:targ][rtype].sort.each do |ref|
              if(not results[entry][:base][rtype].include?(ref))
                n_add << ("  + #{entry} #{rtype} #{ref}")
              end
            end
          end
        end
      end

      n_sub.each {|s| print_status(s) }
      n_add.each {|s| print_status(s) }
    end

  end

end
